/**
 * 
 */
package model;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

/**
 * @author Bushi
 * 
 */
public class PresentDistributorImpl implements PresentDistributor {

	private Map<Integer, Present> presents = new Hashtable<Integer, Present>();
	private Map<String, Category> participants = new Hashtable<String, Category>();

	/*
	 * (non-Javadoc)
	 * 
	 * @see model.PresentDistributor#addPresent(java.lang.Integer,
	 * java.lang.String[])
	 */
	@Override
	public void addPresent(Integer presentNumber, String[] names) {
		if (presents.containsKey(presentNumber)) {
			throw new IllegalArgumentException("The presentNumber already exists. Choose another Number");
		}
		Category category = parseCategory(names);
		for (String name : names) {
			if (!participants.containsKey(name)) {
				throw new IllegalArgumentException("Der Name " + name + " ist kein Wichtelspiel Teilnehmer");
			}
		}
		presents.put(presentNumber, new PresentImpl(presentNumber, names, category));
	}

	private Category parseCategory(String[] names) {
		if (names == null || names.length == 0) {
			return Category.BOTH;
		} else {
			Category gen = participants.get(names[0]);
			for (int i = 1; i < names.length; i++) {
				if (participants.get(names[i]) == gen) {
					continue;
				} else {
					return Category.BOTH;
				}
			}
			return gen;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see model.PresentDistributor#removePresent(java.lang.Integer)
	 */
	@Override
	public boolean removePresent(Integer presentNumber) {
		return presents.remove(presentNumber) != null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see model.PresentDistributor#getPresentDistributions()
	 */
	@Override
	public Hashtable<String, Present> getPresentDistribution() {
		List<String> names = new ArrayList<String>(participants.keySet());
		Hashtable<Integer, Present> presentMap = new Hashtable<Integer, Present>();
		List<Present> presents = new ArrayList<Present>();
		Present pTemp;
		for (Present p : this.presents.values()) {
			pTemp = p.clone();
			presents.add(pTemp);
			presentMap.put(pTemp.getNumber(), pTemp);
		}
		return distribute(names, presentMap);
	}

	private Hashtable<String, Present> distribute(List<String> names, Hashtable<Integer, Present> presentMap) {
		if (names.size() == 0)
			return new Hashtable<String, Present>();
		Hashtable<String, Present> result = new Hashtable<String, Present>();
		// use heuristic "persons with least presents first"
		String currentName = getPersonWithLeastPresents(presentMap.values(), names);
		Present[] ps = getPresents(presentMap.values(), currentName);
		// done via backtracing
		if (ps.length == 0) {
			// there are still people without a present and there is no present
			// left for them
			// throw exception and take one step back
			throw new RuntimeException();
		}
		Present p;
		boolean foundSolution = false;
		for (int i = 0; i < ps.length; i++) {
			p = ps[i];
			// put the name-present tuple into the solution
			result.put(currentName, p);
			names.remove(currentName);
			List<Present> modifiedPresents = removeNameFromPresents(presentMap.values(), currentName);
			presentMap.remove(p.getNumber());
			try {
				// go one step deeper
				result.putAll(distribute(names, presentMap));
				// no exception was thrown so solution was found
				foundSolution = true;
				break;
			} catch (RuntimeException e) {
				// going one step deeper failed... roll back the changes and try
				// another present
				result.remove(currentName);
				names.add(currentName);
				presentMap.put(p.getNumber(), p);
				addNameToPresents(modifiedPresents, currentName);
			}
		}
		if (foundSolution) {
			return result;
		} else {
			return null;
		}
	}

	private void addNameToPresents(List<Present> modifiedPresents, String currentName) {
		for (Present p : modifiedPresents) {
			p.addParticipant(currentName);
		}
	}

	private List<Present> removeNameFromPresents(Collection<Present> presents, String currentName) {
		List<Present> modifiedPresents = new ArrayList<Present>();
		for (Present p : presents) {
			if (p.removeParticipant(currentName)) {
				modifiedPresents.add(p);
			}
		}
		return modifiedPresents;
	}

	private Hashtable<String, Present> distributeError(List<String> names, Hashtable<Integer, Present> presentMap, Hashtable<String, Present> actualOutput) {
		if (names.size() == 0)
			return new Hashtable<String, Present>();
		Hashtable<String, Present> output = new Hashtable<String, Present>();
		String actualName = getPersonWithLeastPresents(presentMap.values(), names);
		Present[] ps = getPresents(presentMap.values(), actualName);
		if (ps.length == 0) {
			throw new RuntimeException();
		}
		for (Present p : ps) {
			if (actualOutput.contains(p)) {
				continue;
			}
			output.put(actualName, p);
			names.remove(actualName);
			presentMap.remove(p.getNumber());
			// removeNameFromPresents(presentMap.values(), actualName);
			try {
				output.putAll(distributeError(names, presentMap, output));
				break;
			} catch (RuntimeException e) {
				output.remove(actualName);
				names.add(actualName);
				presentMap.put(p.getNumber(), p);
			}
		}
		if (output.equals(actualOutput))
			throw new RuntimeException();
		return output;
	}

	public String getPersonWithLeastPresents(Collection<Present> collection, List<String> openNames) {
		String output = "";
		int count = Integer.MAX_VALUE;
		for (String name : openNames) {
			if (getPresents(collection, name).length < count) {
				count = getPresents(collection, name).length;
				output = name;
			}
		}
		return output;
	}

	@Override
	public Present[] getPresents(String name) {
		return getPresents(presents.values(), name);
	}

	private Present[] getPresents(Collection<Present> presents, String name) {
		List<Present> output = new ArrayList<Present>();
		List<String> temp = new ArrayList<String>();
		for (Present p : presents) {
			temp.addAll(Arrays.asList(p.getNames()));
			if (temp.contains(name)) {
				output.add(p);
			}
			temp.clear();
		}
		return output.toArray(new Present[0]);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see model.PresentDistributor#getPresent(java.lang.Integer)
	 */
	@Override
	public Present getPresent(Integer presentNumber) {
		return presents.get(presentNumber);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see model.PresentDistributor#savePresents(java.io.File)
	 */
	@Override
	public void savePresents(File file) throws IOException {

		if (!file.exists()) {
			file.createNewFile();
		}
		// BufferedWriter out = new BufferedWriter(new FileWriter(file));
		FileOutputStream fos = new FileOutputStream(file);
		ObjectOutputStream outStream = new ObjectOutputStream(fos);
		outStream.writeObject(participants);
		outStream.writeObject(presents);
		outStream.flush();
		outStream.close();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see model.PresentDistributor#loadPresents(java.io.File)
	 */
	@SuppressWarnings("unchecked")
	@Override
	public void loadPresents(File file) throws IOException, ClassNotFoundException {
		if (!file.exists()) {
			throw new IOException("The specified file does not exist.");
		}

		// BufferedReader in = new BufferedReader(new FileReader(file));
		// Create a stream for reading.
		FileInputStream fis = new FileInputStream(file);

		// Next, create an object that can read from that file.
		ObjectInputStream inStream = new ObjectInputStream(fis);
		Object o = inStream.readObject();
		if (o instanceof Map<?, ?>) {
			participants = (Map<String, Category>) o;
		}
		o = inStream.readObject();
		if (o instanceof Map<?, ?>) {
			presents = (Map<Integer, Present>) o;
		}
		inStream.close();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see model.PresentDistributor#isValidDistribution()
	 */
	@Override
	public boolean isValidDistribution() {
		return getParticipantsWithoutPresents().length == 0;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see model.PresentDistributor#getFreePresentNumber()
	 */
	@Override
	public Integer getFreePresentNumber() {
		Integer max = 0;
		for (Integer i : getAllPresentNumbers()) {
			max = i > max ? i : max;
		}
		return max + 1;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see model.PresentDistributor#getAllPresentNumbers()
	 */
	@Override
	public Integer[] getAllPresentNumbers() {
		return presents.keySet().toArray(new Integer[0]);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see model.PresentDistributor#getAllPresents()
	 */
	@Override
	public Present[] getAllPresents() {
		return presents.values().toArray(new Present[0]);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see model.PresentDistributor#deleteParticipant(java.lang.String)
	 */
	@Override
	public boolean deleteParticipant(String name) {
		return participants.remove(name) != null;
	}

	@Override
	public String[] getParticipantsWithoutPresents() {
		ArrayList<String> output = new ArrayList<String>();
		// iterate over all participants
		boolean hasPresent;
		for (String participant : participants.keySet()) {
			hasPresent = false;
			for (Present p : presents.values()) {
				if (p.isForParticipant(participant)) {
					hasPresent = true;
				}
			}
			if (!hasPresent) {
				output.add(participant);
			}
		}
		return output.toArray(new String[0]);
	}

	@Override
	public void addParticipant(String name, Category gender) {
		participants.put(name, gender);

	}

	@Override
	public String[] getAllParticipants() {
		return participants.keySet().toArray(new String[0]);
	}

	@Override
	public String toString() {
		StringBuilder output = new StringBuilder();
		for (String name : participants.keySet()) {
			output.append(name);
			output.append(": ");
			for (Present present : getPresents(name)) {
				output.append(present.getNumber());
				output.append(", ");
			}
			output.append("\n");
		}
		return output.toString();
	}

}
